#include <Kharlia/Engine/Common.h>
#include <Kharlia/Engine/Engine.h>
#include <Kharlia/Engine/Init.h>
#include <Kharlia/Engine/Utils.h>
#include <Kharlia/Engine/TileData.h>
#include <Kharlia/Engine/Level.h>
#include <Kharlia/Engine/Actor.h>
#include <Kharlia/Engine/World.h>
#include <Kharlia/Engine/Net.h>
#include <Kharlia/Engine/Player.h>
#include <Kharlia/Engine/Client.h>
#include <Kharlia/Core/IO.h>
#include <Kharlia/Core/App.h>
#include <Kharlia/Core/ObjectUtil.h>
#include <sstream>
#include <iostream>
#include <Kharlia/Engine/Debug.h>

#define DEFAULT_CLIENT_TICK_RATE 30

#define ENABLE_FRAME_COUNTER 1
#define ENABLE_TEST_DIALOG 0
#define ENABLE_MAIN_RENDERVIEW 0
#define ENABLE_SCALING 0
#define ENABLE_CONSOLE 0

#define ED_BRUSH_LAYER_INDEX 300

namespace kharlia { namespace engine {
    
using ui::URect;
using ui::UVector2;
using ui::UDim;

//-----------
// CEngine
//-----------

CEngine* CEngine::msInstance = nullptr;

CEngine::CEngine(PyObject* self):
    CSubsystem(self),
    mInited(false),
    mInitedClient(false),
    mIsServer(true),
    mIsClient(false),
    mIsEditor(false),
    mTickElapsed(0.0),
    mWorld(),
    mLocalPlayer(),
    mResourceManager(),
    mClient(),
    mClientTickRate(0),
    mClientTickElapsed(0.0),
    mNetPeer(nullptr),

    // Editor
    mEditorEnabled(false),
    mEditorClampBrushPos(true),
    mEditorBrush(),
    mEditorBrushVisible(false),
    mEditorBrushPos(FVector2F(0,0)),
    mEditorBrushLayer(),
    mEditorGridVisible(false),
    mEditorSelection(0, 0, 0, 0),
    mEditorSelectionVisible(false)
{
    mLogger = core::FLogger::GetLogger("engine");
}

CEngine::~CEngine() {
    assert(!mInited);
    //if (mInited)
    //    throw RuntimeError("bad state");
}

void CEngine::InitImpl() {
    if (mInited)
        return;
    if (msInstance)
        throw RuntimeError("CEngine instance already exists");
    mResourceManager = make_shared<FResourceManager>();
    mLocalPlayer = extract<TPtr<CPlayer>>(GetLocalPlayerClass()());
    mInited = true;
    msInstance = this;
}

void CEngine::ExitImpl() {
    //if (!mInited)
    //    return;

    ExitClient();

    if (mNetPeer) {
        mNetPeer->Shutdown();
        delete mNetPeer;
        mNetPeer = nullptr;
    }

    if (mWorld) {
        mWorld->ClearLevels(true);
        mWorld->Clear();
        mWorld.Reset();
    }

    mLocalPlayer.Reset();

    if (mResourceManager) {
        mResourceManager->UnloadAll();
        mResourceManager->UnregisterAll();
        mResourceManager.reset();
    }

    mConfig = object();

    msInstance = nullptr;
    mInited = false;
}

void CEngine::InitClient(object window) {
    if (mInitedClient)
        return;

    object vpcls = GetInnerModuleAttr("CClientViewport");
    if (!PyObject_IsInstance(window.ptr(), vpcls.ptr()))
        throw TypeError("viewport must be instance of CClientViewport");

    mClient = extract<TPtr<CClient>>(GetClientClass()());
    mClient->Init(GetSelfPtr<CEngine>(), window);
    mIsClient = true;

    mInitedClient = true;
}

void CEngine::ExitClient() {
    //if (!mInitedClient)
    //    return;

    if (mClient) {
        mClient->Exit();
        mClient.Reset();
    }
    mIsClient = false;

    mInitedClient = false;
}

bool CEngine::EnsureLocalPlayer() {
    if (mLocalPlayer)
        return true;
    if (!mIsClient || !mWorld)
        return false;
    return true;
}

void CEngine::TaskletMain() {
    float delta = 0.0;
    while (mInited) {
        Tick(delta);
        delta = core::CTasker::StaticYield();
    }
}

void CEngine::Tick(float deltaTime) {
    if (mNetPeer)
        mNetPeer->Tick(deltaTime);
    if (mWorld)
        mWorld->Tick(deltaTime);
    if (mInitedClient) {
        mClientTickElapsed += deltaTime;
        if (mClientTickRate == 0 || mClientTickElapsed >= (1.0f / float(mClientTickRate))) {
            mClient->Tick(mClientTickElapsed);
            mClientTickElapsed = 0.0f;
        }
    }
}

bool CEngine::ExecImpl(object cmd, object out) {
    return false;
}

bool CEngine::Exec(object cmd, object out) {
    return call_method<bool>(mSelf, "Exec", cmd, out);
}

TPtr<CWorld> CEngine::LoadWorld(object path) {
    return call_method<TPtr<CWorld>>(mSelf, "LoadWorld", path);
}

void CEngine::SetWorld(TPtr<CWorld> const& world) {
    if (mWorld) {
        mWorld->RemovedFromEngine(GetSelfPtr<CEngine>());
        mWorld.Reset();
    }
    if (world) {
        mWorld = world;
        mWorld->AddedToEngine(GetSelfPtr<CEngine>());
    }
    else
        assert(!mWorld);
}

void CEngine::Connect(str host, uint16 port) {
    if (mNetPeer)
        mNetPeer->Shutdown();
    else
        mNetPeer = new FNetPeer();

    mNetPeer->SetHandler(this);
    mNetPeer->Connect(host, port);
}

void CEngine::OnServerConnected(rn::RakNetGUID guid, rn::SystemAddress addr) {
    //mIsServer = false;
    assert(mNetPeer && mWorld);

    // World takes ownership of the net peer
    FNetPeer* peer = mNetPeer;
    peer->SetHandler(nullptr);
    mNetPeer = nullptr;
    mWorld->NetConnected(peer, guid, addr);
}

void CEngine::OnKeyDown(ui::Key::Scan keycode) {
    call_method<void>(mSelf, "OnKeyDown", keycode);
}

void CEngine::OnKeyUp(ui::Key::Scan keycode) {
    call_method<void>(mSelf, "OnKeyUp", keycode);
}

void CEngine::OnChar(ui::utf32 codepoint) {
    call_method<void>(mSelf, "OnChar", codepoint);
}

void CEngine::SetEditorBrush(shared_ptr<FTileData> const& b) {
    mEditorBrush = b;
    if (mEditorBrush)
        mEditorBrushLayer = make_shared<FTileLayer>(mEditorBrush);
}

KH_BEGIN_GC(CEngine)
    KH_GC_TPTR(thisx->mWorld)
    KH_GC_OBJECT(thisx->mConfig)
    KH_GC_TPTR(thisx->mLocalPlayer)
    KH_GC_TPTR(thisx->mClient)
KH_END_GC()

void _InitEngineClasses() {
    _InitEngineBindings();
}

}} // namespace Kharlia